package app

import (
	"context"
	"errors"
	log "gitee.com/vrv_media/go-micro-framework/pkg/logger"
	"gitee.com/vrv_media/go-micro-framework/registry"
	"gitee.com/vrv_media/go-micro-framework/server"
	"golang.org/x/sync/errgroup"
	"os"
	"os/signal"
	"sync"
)

type App struct {
	// struct 类型 说明是实例化后的  可以直接用 不需要再次声明
	lk       sync.Mutex
	opts     *Options
	cancel   func()
	instance *registry.ServiceInstance // registry 参数-push-test
}

func New(opts ...Option) *App {
	o := DefaultOptions()

	if len(opts) > 0 {
		// 执行附加参数
		for _, opt := range opts {
			opt(o)
		}
	}
	return &App{
		opts: o,
	}
}

// Run 启动整个微服务
func (a *App) Run() error {
	// 生成注册的信息，一开始就生成，后面有注册对象的时候可以直接使用
	instance, err := a.buildServerInstance()
	if err != nil {
		return err
	}
	// 这个变量可能被其他的 goroutine 访问到
	a.lk.Lock()
	a.instance = instance
	a.lk.Unlock()

	// 启动 restServer 和 rpcServer
	eg, startCtx := a.startServers()
	// 注册服务到注册中心
	if a.opts.registrar != nil {

		ctx, cancelFunc := context.WithTimeout(context.Background(), a.opts.registrarTimeout)
		defer cancelFunc()
		err := a.opts.registrar.Register(ctx, a.instance)
		if err != nil {
			log.ErrorF("register service error: %s", err)
			return err
		}

	}

	// 监听退出信号
	// 监听退出信号
	c := make(chan os.Signal)
	signal.Notify(c, a.opts.sigs...)
	eg.Go(func() error {
		select {
		case <-startCtx.Done(): // 主动关闭
			return startCtx.Err()
		case <-c: // 获取退出信号
			return a.Stop() // 执行退出
		}
	})

	if err := eg.Wait(); err != nil {
		return err
	}
	return nil
}

// Stop 停止服务
func (a *App) Stop() error {
	a.lk.Lock()
	instance := a.instance
	a.lk.Unlock()
	if a.opts.registrar != nil && instance != nil {
		// 这里需要强制退出，避免收到退出命令一直退出不了
		ctx, cancelFunc := context.WithTimeout(context.Background(), a.opts.registrarTimeout)
		defer cancelFunc()
		err := a.opts.registrar.Deregister(ctx, instance)
		if err != nil {
			log.ErrorF("deregister service error: %s", err)
			return err
		}
	}
	// 给server发送退出消息
	if a.cancel != nil {
		a.cancel()
	}
	return nil
}

func (a *App) startServers() (*errgroup.Group, context.Context) {
	//解决多个server的同时启动情况的问题分析
	/*
		    现在启动了两个server，一个是restserver，一个是rpcserver
			这两个server是否必须同时启动成功？
			如果有一个启动失败，那么我们就要停止另外一个server
			如果启动了多个， 如果其中一个启动失败，其他的应该被取消
				如果剩余的server的状态：
					1. 还没有开始调用start
						stop
					2. start进行中
						调用进行中的cancel
					3. start已经完成
						调用stop
			如果服务启动了然后这个时候用户立马进行了访问
	*/

	// 生成退出是调用的cancel方法
	canCtx, cancelFunc := context.WithCancel(context.Background())
	a.cancel = cancelFunc
	// 使用errgroup包可以解决这一问题：
	eg, ctx := errgroup.WithContext(canCtx)
	// 服务的协程启动后会进行阻塞，会导致后面的代码也没法执行，所以需要一个wg来判断是否已经启动
	wg := sync.WaitGroup{}
	for _, srv := range a.opts.servers {
		// 用一个临时变量，这样 协程中调用的 srv 就会引用函数内部的变量  不会因为 srv 的改变而改变
		// 不做此操作 有可能发生 下面协程 srv.Start 中 srv 启动的是 其他的 微服务 而没启动 本身的微服务
		s := srv
		// 先启动一个 groutine 去监听是否有 err 产生，如果某个协程产生了error，就将其对应的server停止
		eg.Go(func() error {
			<-ctx.Done() // wait for stop signal
			// 接收到 关闭信号后 先设置一个拥有超时功能的 ctx
			// 不可能无休止的等待 srv.Stop 执行完成, 如果超时 强制退出 (需要内部 Stop 写好相关逻辑)
			sctx, cancel := context.WithTimeout(context.Background(), a.opts.stopTimeout)
			defer cancel()
			return s.Stop(sctx)
		})
		wg.Add(1)
		eg.Go(func() error {
			// 这里出现错误，就会通知同一个parentCtx下面的所有协程，也就是说所有的服务都会收到退出的指令
			log.Info("start rest server")
			wg.Done()
			sctx, cancel := context.WithTimeout(context.Background(), a.opts.startTimeout)
			defer cancel()
			return s.Start(sctx)
		})
	}
	// 上面 api 和 grpc 服务都正常开启后 才能继续运行 否则会 hold 住
	wg.Wait()
	return eg, ctx
}

// 创建服务注册结构体
func (a *App) buildServerInstance() (*registry.ServiceInstance, error) {
	endpoints := make([]string, 0, len(a.opts.endpoints))
	if len(a.opts.endpoints) > 0 {
		for _, e := range a.opts.endpoints {
			endpoints = append(endpoints, e.String())
		}
	}

	// 如果endpoints为0，尝试从服务中获取
	if len(endpoints) == 0 {
		for _, srv := range a.opts.servers {
			if r, ok := srv.(server.Endpointer); ok {
				e, err := r.Endpoint()
				if err != nil {
					return nil, err
				}
				endpoints = append(endpoints, e.String())
			}
		}
	}
	if len(endpoints) == 0 {
		return nil, errors.New("registry endpoints is empty")
	}
	return &registry.ServiceInstance{
		ID:        a.opts.id,
		Name:      a.opts.name,
		Endpoints: endpoints,
		Tags:      a.opts.tags,
	}, nil
}
